#require 'spec_helper'
require 'msf/core'

RSpec.describe Msf::Exploit::Remote::BrowserExploitServer do

  let(:in_memory_profile) do
   {
    "BAP.1433806920.Client.blLGFIlwYrxfvcY" =>
      {
        source: "script",
        os_name: "Windows 8.1",
        os_vendor:  "undefined",
        os_device: "undefined",
        ua_name: "Firefox",
        ua_ver: "35.0",
        arch: "x86",
        java: "1.7",
        silverlight: "false",
        flash: "14.0",
        vuln_test: "true",
        proxy: false,
        language: "en-US,en;q=0.5",
        tried: true,
        activex: [{"clsid"=>"{D27CDB6E-AE6D-11cf-96B8-444553540000}", "method"=>"LoadMovie"}]
   }}
  end

  let(:default_note_type_prefix) do
    in_memory_profile.keys.first.split('.')[0,3] * "."
  end

  let(:first_profile_tag) do
    in_memory_profile.keys.first.split('.')[3]
  end

  let(:first_profile_info) do
    in_memory_profile.values.first
  end

  let(:cli) do
    sock_class = Class.new do
      include Rex::Socket::Tcp
    end

    sock = sock_class.new
    allow(sock).to receive(:peerhost).and_return('0.0.0.0')
    allow(sock).to receive(:peerport).and_return(4444)

    sock
  end

  let(:user_agent) do
    'Mozilla/5.0 (Windows NT 6.3; rv:39.0) Gecko/20100101 Firefox/35.0'
  end

  let(:cli_request) do
    req = Rex::Proto::Http::Request.new
    req.headers['Cookie'] = cookie
    req.headers['User-Agent'] = user_agent
    req
  end

  let(:tag) do
    'tag'
  end

  let(:cookie) do
    "__ua=#{tag};"
  end

  let(:shortname) do
    'browser_exploit_server'
  end

  def create_fake_note(tag, data)
    note = double('note')
    allow(note).to receive(:ntype).and_return(tag)
    allow(note).to receive(:data).and_return(data)

    note
  end


  before(:example) do
    allow_any_instance_of(described_class).to receive(:vprint_status)
    allow_any_instance_of(described_class).to receive(:vprint_line)
    @notes = [create_fake_note(first_profile_tag, in_memory_profile)]
  end

  subject(:server) do
    mod = Msf::Exploit::Remote.allocate
    mod.extend described_class
    mod.send(:initialize)
    mod.send(:datastore=, {'NoteTypePrefix' => default_note_type_prefix})
    allow(mod).to receive(:shortname).and_return(shortname)
    allow(mod).to receive(:fullname).and_return(fullname)
    allow(mod).to receive(:report_client)
    mod
  end

  let(:fullname) do
    'auxiliary/server/browser_autopwn2'
  end

  let(:service_double) do
    service = double('service')
    allow(service).to receive(:server_name=)
    allow(service).to receive(:add_resource)
    service
  end

  let(:exploit_page) do
    server.instance_variable_get(:@exploit_receiver_page)
  end

  before do
    allow(Rex::ServiceManager).to receive(:start).and_return(service_double)
  end

  before(:example) do
    server.start_service
  end

  it_should_behave_like 'Msf::Exploit::JSObfu'


  describe '#get_module_resource' do
    it "should give me a URI to access the exploit page" do
      module_resource = server.get_module_resource
      expect(module_resource).to include(exploit_page)
    end
  end

  describe '#has_bad_activex?' do
    context 'when there is a bad activex' do
      let(:js_ax_value) { "#{first_profile_info[:activex][0][:clsid]}=>#{first_profile_info[:activex][0][:method]}=>false" }
      it 'returns false' do
        expect(server.has_bad_activex?(js_ax_value)).to be_truthy
      end
    end

    context 'when there is no bad activex' do
      let(:js_ax_value) { "#{first_profile_info[:activex][0][:clsid]}=>#{first_profile_info[:activex][0][:method]}=>true" }
      it 'returns true' do
        expect(server.has_bad_activex?(js_ax_value)).to be_falsey
      end
    end
  end

  describe '#on_request_exploit' do
    it "raises a NoMethodError if called" do
      fake_cli = nil
      fake_request = nil
      fake_browser_info = nil
      expect {
        server.on_request_exploit(fake_cli, fake_request, fake_browser_info)
      }.to raise_error(NoMethodError)
    end
  end

  describe '#get_target' do
    it "returns a target" do
      expected_object = double('Msf::Module::Target')
      server.instance_variable_set(:@target, expected_object)
      expect(server.get_target).to eq(expected_object)
    end
  end


  describe 'extract_requirements' do
    context 'when a recognizable requirement is given' do
      it 'returns a hash that contains the recognizable requirement' do
        expected_hash = {:os_name=>'Linux'}
        expect(server.extract_requirements(expected_hash)).to eq(expected_hash)
      end
    end

    context 'when a unrecognizable requirement is given' do
      it 'returns a hash that does not have the unrecognizable requirement' do
        bad_hash = {'UNKNOWN_KEY'=>'VALUE'}
        expect(server.extract_requirements(bad_hash)).to be_empty
      end
    end
  end

  describe '#retrieve_tag' do
    context 'when the browser has a cookie that contains our tag' do
      it 'returns the tag from the cookie' do
        expect(server.retrieve_tag(cli, cli_request)).to eq(tag)
      end
    end

    context 'when the browser does not have a tag' do

      let(:cli_request) do
        Rex::Proto::Http::Request.new
      end

      it 'returns a new one in MD5' do
        expect(server.retrieve_tag(cli, cli_request)).to match(/^[0-9a-f]{32}$/)
      end
    end
  end

  describe '#on_request_uri' do
    before(:example) do
      allow(server).to receive(:process_browser_info)
      allow(server).to receive(:send_response)      { @send_response_called = true }
      allow(server).to receive(:send_redirect)      { @send_redirect_called = true }
      allow(server).to receive(:send_not_found)     { @send_not_found_called = true}
      allow(server).to receive(:on_request_exploit) { @on_request_exploit_called = true }
      allow(server).to receive(:on_request_exploit) { @on_request_exploit_called = true }
    end

    after(:example) do
      @send_response_called      = false
      @send_redirect_called      = false
      @on_request_exploit_called = false
      @send_not_found_called     = false
      @on_request_exploit_called = false
      @report_client             = nil
    end


    context 'when info_receiver_page is requested' do
      it 'sends an empty page' do
        info_receiver_page_var = server.instance_variable_get(:@info_receiver_page)
        cli_request = Rex::Proto::Http::Request.new
        cli_request.uri = info_receiver_page_var
        server.on_request_uri(cli, cli_request)
        expect(@send_response_called).to be_truthy
      end
    end

    context 'when noscript_receiver_page is requested' do
      it 'sends a not-found' do
        noscript_receiver_page_var = server.instance_variable_get(:@noscript_receiver_page)
        cli_request = Rex::Proto::Http::Request.new
        cli_request.uri = noscript_receiver_page_var
        server.on_request_uri(cli, cli_request)
        expect(@send_not_found_called).to be_truthy
      end
    end

  end

  describe '#get_payload' do
    before(:example) do
      target = double('Msf::Module::Target')
      allow(target).to receive(:arch).and_return(nil)
      allow(server).to receive(:get_target).and_return(target)
    end

    let(:encoded) { '@EXE@' }

    let(:x86_payload) {
      double(:encoded => encoded, :arch => ['x86'])
    }

    let(:normalized_profile_info) {
      first_profile_info.inject({}){|data,(k,v)| data[k.to_sym] = v; data}
    }

    context 'when the payload supports the visitor\'s browser architecture' do
      it 'returns a payload' do
        allow(server).to receive(:regenerate_payload).and_return(x86_payload)
        expect(server.get_payload(cli, normalized_profile_info)).to eq(encoded)
      end
    end
  end

  describe '#browser_profile_prefix' do
    it 'returns a BES prefix' do
      expect(subject.browser_profile_prefix).to include(shortname)
    end
  end

  describe '#get_custom_404_url' do
    let(:custom_404) do
      'http://example.com'
    end

    before(:example) do
      allow(subject).to receive(:datastore).and_return({'Custom404'=>custom_404})
    end

    context 'when a custom 404 URL is set' do
      it 'returns the URL' do
        expect(subject.get_custom_404_url).to eq(custom_404)
      end
    end
  end

  describe '#get_module_uri' do
    let(:exploit_receiver_page) do
      'exploit_receiver_page'
    end

    before(:example) do
      subject.instance_variable_set(:@exploit_receiver_page, exploit_receiver_page)
      allow(subject).to receive(:get_uri).and_return('')
    end

    it 'returns a module URI' do
      expect(subject.get_module_uri).to include(exploit_receiver_page)
    end
  end

  describe '#try_set_target' do
    let(:aux_mod) do
      mod = Msf::Auxiliary.allocate
      mod.extend described_class
      mod.send(:initialize)
      mod
    end

    let(:target_options) do
      {ua_name: 'Firefox'}
    end

    let(:target) do
      t = double('target')
      allow(t).to receive(:opts).and_return(target_options)
      t
    end

    let(:default_auto_target) do
      # The default auto target is always the first on the list.
      # In a module this would be the "Automatic" target.
      t = double('target')
      allow(t).to receive(:opts).and_return({})
      t
    end

    let(:targets) do
      [ default_auto_target, target ]
    end

    context 'when an auxiliary uses BES' do
      it 'returns nil' do
        expect(aux_mod.try_set_target(first_profile_info)).to be_nil
      end
    end

    context 'when an exploit uses BES' do
      it 'sets the instance variable @target' do
        expect(subject.instance_variable_get(:@target)).to be_nil
        allow(subject).to receive(:targets).and_return(targets)
        subject.try_set_target(first_profile_info)
        expect(subject.instance_variable_get(:@target)).to eq(target)
      end
    end
  end

  describe '#get_bad_requirements' do
    context 'when there is a bad requirement' do
      it 'returns a bad requirement' do
        requirements = { ua_ver: '34' }
        subject.instance_variable_set(:@requirements, requirements)
        expect(subject.get_bad_requirements(first_profile_info)).to include(:ua_ver)
      end
    end

    context 'when there is no bad requirement' do
      it 'returns an empty hash' do
        requirements = { ua_ver: first_profile_info[:ua_ver] }
        subject.instance_variable_set(:@requirements, requirements)
        expect(subject.get_bad_requirements(first_profile_info)).to be_empty
      end
    end
  end

  describe '#process_browser_info' do
    before(:example) do
      allow(subject).to receive(:report_client) { |args| @report_client = args }
      allow(subject).to receive(:browser_profile).and_return(Hash.new)
    end

    context 'when source is :script' do
      context 'when no profile is found' do
        it 'reports an empty ua_ver' do
          subject.process_browser_info(:script, cli, cli_request)
          expect(@report_client[:ua_ver]).to eq('[]')
        end
      end
    end

    context 'when source is :headers' do
      context 'when user-agent says the browser is FF 35.0' do
        it 'reports ua_ver as 35.0' do
          subject.process_browser_info(:headers, cli, cli_request)
          expect(@report_client[:ua_ver]).to eq('35.0')
        end
      end
    end
  end

  describe '#has_proxy?' do
    context 'when there is no proxy' do
      it 'returns false' do
        expect(subject.has_proxy?(cli_request)).to be_falsey
      end
    end
  end

  describe '#cookie_name' do
    before(:example) do
      subject.datastore.merge!({'CookieName'=>cookie})
    end

    it 'returns a cookiename' do
      expect(subject.cookie_name).to eq(cookie)
    end
  end

  describe '#cookie_header' do
    it 'returns a cookie' do
      tag = 'TAG'
      expect(subject.cookie_header(tag)).to include(tag)
    end
  end

end
